package link.thingscloud.freeswitch.esl;


import link.thingscloud.freeswitch.esl.inbound.NettyInboundClient;
import link.thingscloud.freeswitch.esl.inbound.option.ConnectState;
import link.thingscloud.freeswitch.esl.inbound.option.InboundClientOption;
import link.thingscloud.freeswitch.esl.inbound.option.ServerOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;

/**
 * FreeSWITCH event socket connection pool.
 *
 * Event Socket is a communication protocol provided by FreeSWITCH that offers a mechanism
 * for real-time event notifications and interaction with FreeSWITCH. Event Socket operates in two modes,
 * Inbound and Outbound, distinguished as follows:
 *
 * Inbound Mode:    FreeSWITCH is the server  <=>  External application is the client
 * Outbound Mode:   FreeSWITCH is the client  <=>  External application is the server
 * Through extensive work experience, the author believes that Inbound mode is more user-friendly
 * and relatively easier to program. Since all application scenarios can be addressed using Inbound mode,
 * this document focuses on explaining the Inbound mode.
 *
 * The Event Socket protocol is a TCP-based application layer text protocol that can use plain text
 * or XML format. Any client supporting TCP Socket can interact with FreeSWITCH. With Event Socket,
 * external applications can listen to internal events within FreeSWITCH, such as call establishment,
 * hang-up, DTMF input, etc. External programs can send commands and control the behavior of the
 * FreeSWITCH, which can be used to create custom telephony applications, call center solutions,
 * conferencing systems, etc.
 *
 * Although FreeSWITCH officially provides Event Socket Libraries (ESL) in various languages,
 * the author has found these existing projects to be either not user-friendly or having various
 * performance issues. The project introduced here implements an Event Socket protocol client
 * based on the Java Netty framework.
 *
 * Why a Connection Pool is Needed ？
 * A connection pool is necessary for high-performance considerations. Let's recall the scenario
 * in Java projects over a decade ago where we directly used the JDBC Connector to connect to MySQL databases:
 *
 * Request comes -> Establish database connection -> Send query or modification request -> Get response -> Close connection
 * Establishing and closing database connections are time-consuming operations, especially under high concurrency.
 * Using a connection pool allows database connections to be created in advance and maintained in the pool,
 * rather than creating a new connection each time the database needs to be accessed.
 * This reduces the number of connection creations and closures, thereby improving performance.
 * Over the years, technology has rapidly evolved, and Java database connection pool projects have continually emerged,
 * from early C3P0 to Druid, and today’s HikariCP. The motivation behind constantly reinventing the wheel largely stems
 * from the continuous pursuit of performance.
 *
 * The development history of the Java MySQL connection pool is provided to facilitate understanding and comparison.
 * We face similar issues in the interaction between the Java client and FreeSWITCH. Additionally,
 * Netty is chosen for its foundation in implementing high-performance java applications.
 *
 * Challenges in Implementing a Connection Pool
 * The challenge in implementing a FreeSWITCH connection pool lies in the fact that various events in FreeSWITCH’s Event Socket
 * communication are asynchronously generated. For example, after a call starts, we need to receive these event messages
 * through the Event Socket protocol: call answer events, DTMF keypresses, speech recognition results, call hang-up events.
 * At this point, we need to maintain a long connection to receive various event messages in real-time, while also using
 * this Socket connection to send various commands. This seemingly means that each call requires a long connection.
 * What if there are 100 calls? Or 1000 calls? Clearly, this is not feasible. Let’s analyze further.
 *
 * If there is a 1:1 correspondence between calls and Event Socket connections, most of the time these socket connections are idle,
 * leading to resource wastage. In a call, we may have the following requirements and characteristics:
 *
 * Each call needs a unique identifier, uuid, which is generated by our client program to control the entire call;
 * During a call, using the Event Socket protocol, some method calls return results immediately, such as execute app,
 * while others are blocking and take a long time, such as api originate. To achieve Socket connection reuse,
 * it is necessary to minimize Socket occupancy time, ideally completing within a few milliseconds or tens of milliseconds;
 * During a call, we can use the asynchronous method bgapi and subscribe to the backgroundId to receive subsequent messages,
 * significantly reducing Socket connection occupancy time;
 *
 * By using methods 2 and 3, we have greatly reduced the Socket connection occupancy time, making it possible to
 * receive FreeSWITCH command responses within milliseconds after sending the command.
 * The next issue is how to asynchronously receive various events. In the analysis of point 1, we noticed that
 * each call has a unique uuid identifier. We can use a default connection to specifically handle receiving all
 * asynchronous event messages for all calls. This default connection object only handles receiving messages and
 * does not send FreeSWITCH commands. Upon receiving various asynchronous events, it distributes messages
 * based on uuid to the corresponding consumers. Before establishing the original call, we create a callback function object,
 * then bind the uuid and the callback function for the call, and register it with the default connection object.
 * This solution perfectly addresses the issue! Thus, the internal structure of the connection pool includes two parts:
 * the default connection object and the connection pool. Their roles are briefly described as follows:
 *
 * Default connection object: Subscribes to asynchronous event messages for all calls and distributes them based on
 *   uuid to different calls, i.e., calls the callback functions registered at the time of each call’s establishment.
 *   There is only one default connection object.
 *
 * Connection pool: A global connection pool internally storing 10 connection objects. Whenever a call thread needs to
 * send a command to FreeSWITCH, it borrows a connection object from the pool. After sending the command and receiving the response,
 * the connection is immediately returned to the pool. The connection objects in the pool do not subscribe to any messages and
 * are only used for sending commands.
 *
 * @author  easycallcenter365@gmail.com
 */
public class EslConnectionPool {
    protected static final Logger logger = LoggerFactory.getLogger(EslConnectionPool.class);

    /**
     *  esl-conn-pool object  for current FreeSWITCH instance
     */
    private final LinkedBlockingDeque<EslConnectionDetail> eslConnectionPool = new LinkedBlockingDeque<>(1000);

    /**
     * default connection for current FreeSWITCH instance.
     *  a default connection is used to handle receiving all
     *  asynchronous event messages for all calls of current FreeSWITCH instance.
     *  It does not send FreeSWITCH commands.
     */
    private EslConnectionDetail defaultEslConnection = null;

    private Semaphore semaphore = null;
    private int connCount = 0;
    private String host;
    private int port;
    private String pass;

    private EslConnectionPool(int connCount, String host, int port, String pass) {
        this.connCount = connCount;
        this.semaphore = new Semaphore(connCount);
        this.host = host;
        this.port = port;
        this.pass = pass;
    }

    public String getEslAddr() {
        return String.format("%s:%d", host, port);
    }


    public int getPoolSize() {
        return connCount;
    }

    private static EslConnectionDetail createOneConn(
            String host,
            int port,
            String pass,
            boolean subscribeEvents) {
        String hostKey = String.format("%s:%d", host, port);
        InboundClientOption inboundOption = new InboundClientOption();
        ServerOption serverOption = new ServerOption(host, port);
        inboundOption.defaultPassword(pass).addServerOption(serverOption);
        if (subscribeEvents) {
            inboundOption.addEvents(EslConnectionDetail.getEventSubscriptions());
        }
        InboundClient conn = new NettyInboundClient(inboundOption);
        conn.start();
        EslConnectionDetail eslConnectionDetail = new EslConnectionDetail(inboundOption, serverOption, conn);
        inboundOption.addListener(eslConnectionDetail);
        String addr = serverOption.addr().intern();
        synchronized (addr.intern()) {
            try {
                addr.wait(3000);
            } catch (Exception e) {
                logger.error("error occurs on waiting for esl connection..." + e.toString());
            }
        }
        if (serverOption.state() != ConnectState.AUTHED) {
            logger.error("Can not connect to FreeSWITCH instance :{}", hostKey);
            return null;
        }
        return eslConnectionDetail;
    }


    public EslConnectionDetail getDefaultEslConn() {
        String hostKey = String.format("%s:%d", host, port);
        if (null == defaultEslConnection) {
            synchronized (hostKey.intern()) {
                if (null == defaultEslConnection) {
                    defaultEslConnection = createOneConn(host, port, pass, true);
                }
            }
        }
        return defaultEslConnection;
    }

    private void setConnections(List<EslConnectionDetail> connList) {
        if (connList.size() != connCount) {
            throw new RuntimeException("The number of connections in connList is greater than " +
                    "the number set by the esl connection pool, this will causes the semaphore to fail!");
        }
        eslConnectionPool.clear();
        eslConnectionPool.addAll(connList);
    }

    public static EslConnectionPool createPool(int poolSize, String host, int port, String pass) {
        ArrayList<EslConnectionDetail> connList = new ArrayList<>(100);
        for (int i = 0; i <= poolSize - 1; i++) {
            EslConnectionDetail conn = EslConnectionPool.createOneConn(
                    host,
                    port,
                    pass,
                    false);
            if (null != conn) {
                connList.add(conn);
            }
        }
        EslConnectionPool connectionPool = new EslConnectionPool(poolSize, host, port, pass);
        connectionPool.setConnections(connList);
        connectionPool.getDefaultEslConn();
        return connectionPool;
    }

    /**
     * obtain a esl connection
     *
     * @return
     */
    public EslConnectionDetail getConnection() {
        try {
            semaphore.acquire();
            return eslConnectionPool.poll();
        } catch (Exception e) {
        }
        return null;
    }

    /**
     * release a esl connection
     *
     * @param conn
     */
    public void releaseOneConn(EslConnectionDetail conn) {
        try {
            eslConnectionPool.add(conn);
            semaphore.release();
        } catch (Exception e) {
        }
    }

}